#!/usr/bin/env python
# encoding: UTF-8

__version__ = "$Revision: 14 $"

import error
import helpers
import maps

class Player(object):
    def __init__(self, name="Unnamed Player", security_digest=None):
        self.name=name
        self.digest = security_digest
        
    def __str__(self):
        return self.name

class Game(object):
    colours = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0x00FFFF]
    
    def __init__(self, name, map, max_players):
        self.players = helpers.DictList()
        self.map = map
        self.name = name
        self.max_players = max_players
        
    def __str__(self):
        return "game %s" % self.name
    
    def add_player(self, player):
        num_players = len(self.players)
        if num_players >= self.max_players:
            assert num_players == self.max_players
            raise error.GameFull(max_players=self.max_players)
        return self.players.append(player)
    
    def remove_player(self, id):
        del self.players[id]

    @property
    def num_players(self):
        return len(self.players)

#~ XMLRPCInterface = []
#~ 
#~ def RPC(func):
    #~ """Decorator used in liaison with XMLRPCInterface array to define publicly
    #~ exposed interface of the GameServer"""
    #~ XMLRPCInterface.append(func.__name__)
    #~ return func
        
class GameServer(object):
    games = None
    admin_code = None
    
    def __init__(self):
        self.games = helpers.DictList()
        self.admin_code = helpers.random_digest()
    
    #~ @RPC
    def join(self, player_name, game_id):
        """join(string player_name, int game_id)
        returns (player_id, player_digest)
        
        player_id is game-specific
        player_digest is a string used to identify the player, some other 
                      methods ask for it"""
        player = Player(player_name)
        player.digest = helpers.random_digest()
        player_id = self.games[game_id].add_player(player)
        return (player_id, player.digest)
    
    #~ @RPC
    def list_games(self):
        """list_games()
        returns list of (game_id, game_name, str(game.map), connected_players,
                         max_number_of_players)
        """
        return [(i, game.name, str(game.map), game.num_players, game.max_players) 
                for i, game in self.games]
    
    def list_players(self, game_id):
        """list_players()
        returns list of (player_id, player_name)"""
        return [(i, player) for i, player in self.games[game_id].players]

    #~ def _dispatch(self, method, params):
        #~ if method in XMLRPCInterface:
            #~ method = getattr(self, method)
            #~ return method(*params)
        #~ else:
            #~ raise AttributeError
            
    #~ @RPC
    def new_game(self, name="Default", map_id=1, max_players=2):
        """new_game(name, map_id, max_players=2)
        returns game_id
        """
        map = maps.maps[map_id]
        game = Game(name, map, max_players)
        return self.games.append(game)
        
    #~ @RPC
    def error_list(self):
        """error_list()
        Returns list of exception names prefixed by module name. The position
        in the list (numbered from 0) corresponds to the fault code.

        The client can import error, catch xml-rpc faults and then 
        raise error.fault_codes[fault_code](constructor_args)
        so there is usually no need to use this function. Non-python clients
        also don't need to use this as the type of the raised exception is 
        contained in the fault string.
        """
        return [".".join((x.__module__, x.__name__)) for x in error.fault_codes]            
    
    #~ @RPC
    def remove_player(self, game_id, player_id, admin_code):
        """remove_player(game_id, player_id, admin_code)
        Returns True if player is removed and raises ValueError if there is 
        no player with this id"""
        if admin_code != self.admin_code:
            raise error.WrongSecurityDigest()
        del self.games[game_id].players[player_id]
        return True
        
    #~ @RPC
    def leave_game(self, game_id, player_id, player_digest):
        """leave_game(game_id, player_id, player_digest)
        Returns True on success or raises WrongSecurityDigest or ValueError.

        player_digest is the string returned by join to authenticate the player
        
        Like remove_player (which needs admin_code), but intended for player
        to intentionally leave the game"""
        if player_digest != self.games[game_id].players[player_id].digest:
            raise error.WrongSecurityDigest()
        return self.remove_player(game_id, player_id, self.admin_code)
    
    #~ @RPC
    def terminate_game(self, game_id, admin_code):
        """terminate_game(game_id, admin_code)
        
        Terminates game with given game_id. On success returns True, 
        otherwise raises IndexError"""
        if admin_code != self.admin_code:
            raise error.WrongSecurityDigest()
        
        del self.games[game_id]
        return True
    
    def end_turn(self, game_id, player_id, player_digest):
        """end_turn(game_id, player_id, player_digest)
        returns True on success or raises NotYourTurn (or raises 
        WrongSecurityDigest)"""
        
        if player_digest != self.games[game_id].players[player_id].digest:
            raise error.WrongSecurityDigest()
        return self.games[game_id].end_turn(player_id)
    
    def move(self, game_id, player_id, player_digest, *passed_args):
        """move(game_id, player_id, player_digest, *passed_args)
        tests the digest and calls game.move(*passed_args) and returns it's
        return value"""
        
        if player_digest != self.games[game_id].players[player_id].digest:
            raise error.WrongSecurityDigest()
            
        return self.games[game_id].move(*passed_args)
    
    def game_status(self, game_id, *passed_args):
        """game_status(game_id, *passed_args)
        returns game.status(*passed_args)
        
        Note that it requires no authentication, thus it is possible to view
        (and possibly log) all currently running games."""
        
        return self.games[game_id].status(*passed_args)


import unittest

class TestGame(unittest.TestCase):
    def setUp(self):
        self.server = GameServer()
        self.server.new_game("Nová", map_id=1, max_players=3)
        self.p0 = self.server.join("Hráč", 0)
    
    def test_admin_code(self):
        self.assert_(len(self.server.admin_code) > 8)    
        oldcode = self.server.admin_code
        self.server = GameServer()
        self.assertNotEqual(self.server.admin_code, oldcode)
    
    def test_player_digest(self):
        self.assert_(len(self.p0[1]) > 8)
        self.assertEqual(self.p0[1], (self.server.games[0].players[0].digest))
        self.p1 = self.server.join("Hráč", 0)
        self.assertNotEqual(self.server.games[0].players[0].digest,
                            self.server.games[0].players[1].digest)
                            
        self.assertRaises(error.WrongSecurityDigest, 
                                self.server.leave_game, 0, 0, self.p1[1])
        self.assertRaises(error.WrongSecurityDigest, 
                                self.server.end_turn, 0, 0, self.p1[1])
        self.assertRaises(error.WrongSecurityDigest, 
                                self.server.move, 0, 0, self.p1[1])
    
    def test_join_and_remove_players(self):
        self.p1 = self.server.join("Hráč", 0)
        self.p2 = self.server.join("Hráč", 0)
        self.server.leave_game(0, 1, self.p1[1])
        
        self.assertRaises(IndexError, self.server.leave_game, 0, 1, self.p1[1])
        self.assertRaises(error.WrongSecurityDigest, 
                                self.server.leave_game, 0, 0, self.p1[1])
        
        self.assertEqual(len(self.server.games), 1)
        self.assertEqual(len(self.server.games[0].players), 2)
        self.assertEqual(self.server.games[0].num_players, 2)
        self.assertEqual(len(self.server.list_games()), 1)
        
        self.p3 = self.server.join("Hr4", 0)
        
        list = self.server.list_players(0)
        list = [(i, str(p)) for i, p in list]
        
        self.assertEquals(list, [(0, "Hráč"),
                                 (2, "Hráč"),
                                 (3, "Hr4")])
  
        self.assertEqual(self.p0[0], 0)
        self.assertEqual(self.p1[0], 1)
        self.assertEqual(self.p2[0], 2)
        self.assertEqual(self.p3[0], 3)
        
        self.assertRaises(error.GameFull, self.server.join, "XY", 0)
        self.assertEqual(self.server.games[0].num_players, 3)
        
        self.server.remove_player(0, 0, self.server.admin_code)
        self.assertEqual(self.server.games[0].num_players, 2)
        
        self.p4 = self.server.join("Hr5", 0)
        self.assertEqual(self.server.games[0].num_players, 3)
        self.assertEqual(self.p4[0], 4)
        
    def test_terminate_game(self):
        self.assertRaises(IndexError, self.server.join, "XY", 1)
        self.server.new_game("Nová", map_id=1, max_players=3)
        self.p2 = self.server.join("Hráč2", 1)
        self.assertEqual(self.server.games[0].num_players, 1)
        self.assertEqual(self.server.games[1].num_players, 1)
        self.server.terminate_game(0, self.server.admin_code)
        self.assertRaises(IndexError, self.server.terminate_game, 0, 
                            self.server.admin_code)
        try:
            self.server.games[0]
        except IndexError:
            pass
        else:
            fail()
        
        self.assertRaises(IndexError, self.server.join, "Hráš", 0)
        
                
if __name__ == "__main__":
    unittest.main()
